package org.apache.tomcat.maven.plugin.tomcat9.run;
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.tomcat.maven.plugin.tomcat9.AbstractTomcat9Mojo;
import org.apache.tomcat.maven.runner.Tomcat9Runner;
import org.apache.tomcat.maven.runner.Tomcat9RunnerCli;
import org.codehaus.plexus.archiver.jar.Manifest;
import org.codehaus.plexus.archiver.jar.ManifestException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.SelectorUtils;

import java.io.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author Olivier Lamy
 * @since 2.0
 */
public abstract class AbstractExecWarMojo
        extends AbstractTomcat9Mojo {

    @Parameter(defaultValue = "${project.artifact}", required = true, readonly = true)
    protected Artifact projectArtifact;

    /**
     * The maven project.
     */
    @Parameter(defaultValue = "${project}", required = true, readonly = true)
    protected MavenProject project;

    @Parameter(defaultValue = "${plugin.artifacts}", required = true)
    protected List<Artifact> pluginArtifacts;

    @Parameter(defaultValue = "${project.build.directory}")
    protected File buildDirectory;
    @Parameter(property = "maven.tomcat.exec.war.tomcatConf", defaultValue = "src/main/tomcatconf")
    protected File tomcatConfigurationFilesDirectory;
    @Parameter(defaultValue = "src/main/tomcatconf/server.xml", property = "maven.tomcat.exec.war.serverXml")
    protected File serverXml;
    /**
     * Name of the generated exec JAR.
     */
    @Parameter(property = "tomcat.jar.finalName",
            defaultValue = "${project.artifactId}-${project.version}-war-exec.jar", required = true)
    protected String finalName;
    /**
     * The webapp context path to use for the web application being run.
     * The name to store webapp in exec jar. Do not use /
     */
    @Parameter(property = "maven.tomcat.path", defaultValue = "${project.artifactId}", required = true)
    protected String path;
    @Parameter
    protected List<WarRunDependency> warRunDependencies;
    @Component
    protected ArtifactResolver artifactResolver;
    /**
     * Maven Artifact Factory component.
     */
    @Component
    protected ArtifactFactory artifactFactory;
    /**
     * Location of the local repository.
     */
    @Parameter(defaultValue = "${localRepository}", required = true, readonly = true)
    protected ArtifactRepository local;
    /**
     * List of Remote Repositories used by the resolver
     */
    @Parameter(defaultValue = "${project.remoteArtifactRepositories}", required = true, readonly = true)
    protected List<ArtifactRepository> remoteRepos;
    @Component
    protected MavenProjectHelper projectHelper;
    /**
     * Attach or not the generated artifact to the build (use true if you want to install or deploy it)
     */
    @Parameter(property = "maven.tomcat.exec.war.attachArtifact", defaultValue = "true", required = true)
    protected boolean attachArtifact;
    /**
     * the classifier to use for the attached/generated artifact
     */
    @Parameter(property = "maven.tomcat.exec.war.attachArtifactClassifier", defaultValue = "exec-war",
            required = true)
    protected String attachArtifactClassifier;
    /**
     * the type to use for the attached/generated artifact
     */
    @Parameter(property = "maven.tomcat.exec.war.attachArtifactType", defaultValue = "jar", required = true)
    protected String attachArtifactClassifierType;
    /**
     * to enable naming when starting tomcat
     */
    @Parameter(property = "maven.tomcat.exec.war.enableNaming", defaultValue = "false", required = true)
    protected boolean enableNaming;
    /**
     * see http://tomcat.apache.org/tomcat-7.0-doc/config/valve.html
     */
    @Parameter(property = "maven.tomcat.exec.war.accessLogValveFormat", defaultValue = "%h %l %u %t %r %s %b %I %D",
            required = true)
    protected String accessLogValveFormat;
    /**
     * list of extra dependencies to add in the standalone tomcat jar: your jdbc driver, mail.jar etc..
     * <b>Those dependencies will be in root classloader.</b>
     */
    @Parameter
    protected List<ExtraDependency> extraDependencies;
    /**
     * list of extra resources to add in the standalone tomcat jar: your logger configuration etc
     */
    @Parameter
    protected List<ExtraResource> extraResources;
    /**
     * Main class to use for starting the standalone jar.
     */
    @Parameter(property = "maven.tomcat.exec.war.mainClass",
            defaultValue = "org.apache.tomcat.maven.runner.Tomcat9RunnerCli", required = true)
    protected String mainClass;
    /**
     * which connector protocol to use HTTP/1.1 or org.apache.coyote.http11.Http11NioProtocol
     */
    @Parameter(property = "maven.tomcat.exec.war.connectorHttpProtocol", defaultValue = "HTTP/1.1", required = true)
    protected String connectorHttpProtocol;
    /**
     * configure a default http port for the standalone jar
     *
     * @since 2.2
     */
    @Parameter(property = "maven.tomcat.exec.war.httpPort")
    protected String httpPort;
    /**
     * File patterns to exclude from extraDependencies
     *
     * @since 2.2
     */
    @Parameter
    protected String[] excludes;
    /**
     * Path under {@link #buildDirectory} where this mojo may do temporary work.
     */
    @Parameter(defaultValue = "${project.build.directory}/tomcat9-maven-plugin-exec")
    private File pluginWorkDirectory;
    /**
     * Skip the execution
     *
     * @since 2.2
     */
    @Parameter(property = "maven.tomcat.skip", defaultValue = "false")
    private boolean skip;

    public void execute()
            throws MojoExecutionException, MojoFailureException {
        if (this.skip) {
            getLog().info("skip execution");
            return;
        }
        //project.addAttachedArtifact(  );
        File warExecFile = new File(buildDirectory, finalName);
        if (warExecFile.exists()) {
            warExecFile.delete();
        }

        File execWarJar = new File(buildDirectory, finalName);

        FileOutputStream execWarJarOutputStream = null;
        ArchiveOutputStream os = null;
        File tmpPropertiesFile = null;
        File tmpManifestFile = null;
        FileOutputStream tmpPropertiesFileOutputStream = null;
        PrintWriter tmpManifestWriter = null;

        try {

            tmpPropertiesFile = new File(buildDirectory, "war-exec.properties");
            if (tmpPropertiesFile.exists()) {
                tmpPropertiesFile.delete();
            }
            tmpPropertiesFile.getParentFile().mkdirs();

            tmpManifestFile = new File(buildDirectory, "war-exec.manifest");
            if (tmpManifestFile.exists()) {
                tmpManifestFile.delete();
            }
            tmpPropertiesFileOutputStream = new FileOutputStream(tmpPropertiesFile);
            execWarJar.getParentFile().mkdirs();
            execWarJar.createNewFile();
            execWarJarOutputStream = new FileOutputStream(execWarJar);

            tmpManifestWriter = new PrintWriter(tmpManifestFile);

            // store :
            //* wars in the root: foo.war
            //* tomcat jars
            //* file tomcat.standalone.properties with possible values :
            //   * useServerXml=true/false to use directly the one provided
            //   * enableNaming=true/false
            //   * wars=foo.war|contextpath;bar.war  ( |contextpath is optionnal if empty use the war name )
            //   * accessLogValveFormat=
            //   * connectorhttpProtocol: HTTP/1.1 or org.apache.coyote.http11.Http11NioProtocol
            //* optionnal: conf/ with usual tomcat configuration files
            //* MANIFEST with Main-Class

            Properties properties = new Properties();

            properties.put(Tomcat9Runner.ARCHIVE_GENERATION_TIMESTAMP_KEY,
                    Long.toString(System.currentTimeMillis()));
            properties.put(Tomcat9Runner.ENABLE_NAMING_KEY, Boolean.toString(enableNaming));
            properties.put(Tomcat9Runner.ACCESS_LOG_VALVE_FORMAT_KEY, accessLogValveFormat);
            properties.put(Tomcat9Runner.HTTP_PROTOCOL_KEY, connectorHttpProtocol);

            if (httpPort != null) {
                properties.put(Tomcat9Runner.HTTP_PORT_KEY, httpPort);
            }

            os = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.JAR,
                    execWarJarOutputStream);

            if ("war".equals(project.getPackaging())) {

                os.putArchiveEntry(new JarArchiveEntry(StringUtils.removeStart(path, "/") + ".war"));
                IOUtils.copy(new FileInputStream(projectArtifact.getFile()), os);
                os.closeArchiveEntry();

                properties.put(Tomcat9Runner.WARS_KEY, StringUtils.removeStart(path, "/") + ".war|" + path);
            } else if (warRunDependencies != null && !warRunDependencies.isEmpty()) {
                for (WarRunDependency warRunDependency : warRunDependencies) {
                    if (warRunDependency.dependency != null) {
                        Dependency dependency = warRunDependency.dependency;
                        String version = dependency.getVersion();
                        if (StringUtils.isEmpty(version)) {
                            version = findArtifactVersion(dependency);
                        }

                        if (StringUtils.isEmpty(version)) {
                            throw new MojoExecutionException(
                                    "Dependency '" + dependency.getGroupId() + "':'" + dependency.getArtifactId()
                                            + "' does not have version specified");
                        }
                        Artifact artifact = artifactFactory.createArtifactWithClassifier(dependency.getGroupId(), //
                                dependency.getArtifactId(), //
                                version, //
                                dependency.getType(), //
                                dependency.getClassifier());

                        artifactResolver.resolve(artifact, this.remoteRepos, this.local);

                        File warFileToBundle = new File(resolvePluginWorkDir(), artifact.getFile().getName());
                        FileUtils.copyFile(artifact.getFile(), warFileToBundle);

                        if (warRunDependency.contextXml != null) {
                            warFileToBundle = addContextXmlToWar(warRunDependency.contextXml, warFileToBundle);
                        }
                        final String warFileName = artifact.getFile().getName();
                        os.putArchiveEntry(new JarArchiveEntry(warFileName));
                        IOUtils.copy(new FileInputStream(warFileToBundle), os);
                        os.closeArchiveEntry();
                        String propertyWarValue = properties.getProperty(Tomcat9Runner.WARS_KEY);
                        String contextPath =
                                StringUtils.isEmpty(warRunDependency.contextPath) ? "/" : warRunDependency.contextPath;
                        if (propertyWarValue != null) {
                            properties.put(Tomcat9Runner.WARS_KEY,
                                    propertyWarValue + ";" + warFileName + "|" + contextPath);
                        } else {
                            properties.put(Tomcat9Runner.WARS_KEY, warFileName + "|" + contextPath);
                        }
                    }
                }
            }

            if (serverXml != null && serverXml.exists()) {
                os.putArchiveEntry(new JarArchiveEntry("conf/server.xml"));
                IOUtils.copy(new FileInputStream(serverXml), os);
                os.closeArchiveEntry();
                properties.put(Tomcat9Runner.USE_SERVER_XML_KEY, Boolean.TRUE.toString());
            } else {
                properties.put(Tomcat9Runner.USE_SERVER_XML_KEY, Boolean.FALSE.toString());
            }

            os.putArchiveEntry(new JarArchiveEntry("conf/web.xml"));
            IOUtils.copy(getClass().getResourceAsStream("/conf/web.xml"), os);
            os.closeArchiveEntry();

            properties.store(tmpPropertiesFileOutputStream, "created by Apache Tomcat Maven plugin");

            tmpPropertiesFileOutputStream.flush();
            tmpPropertiesFileOutputStream.close();

            os.putArchiveEntry(new JarArchiveEntry(Tomcat9RunnerCli.STAND_ALONE_PROPERTIES_FILENAME));
            IOUtils.copy(new FileInputStream(tmpPropertiesFile), os);
            os.closeArchiveEntry();

            // add tomcat classes
            for (Artifact pluginArtifact : pluginArtifacts) {
                if (StringUtils.equals("org.apache.tomcat", pluginArtifact.getGroupId()) //
                        || StringUtils.equals("org.apache.tomcat.embed", pluginArtifact.getGroupId()) //
                        || StringUtils.equals("org.eclipse.jdt.core.compiler", pluginArtifact.getGroupId()) //
                        || StringUtils.equals("commons-cli", pluginArtifact.getArtifactId()) //
                        || StringUtils.equals("tomcat9-war-runner", pluginArtifact.getArtifactId())) {
                    JarFile jarFile = new JarFile(pluginArtifact.getFile());
                    extractJarToArchive(jarFile, os, null);
                }
            }

            // add extra dependencies
            if (extraDependencies != null && !extraDependencies.isEmpty()) {
                for (Dependency dependency : extraDependencies) {
                    String version = dependency.getVersion();
                    if (StringUtils.isEmpty(version)) {
                        version = findArtifactVersion(dependency);
                    }

                    if (StringUtils.isEmpty(version)) {
                        throw new MojoExecutionException(
                                "Dependency '" + dependency.getGroupId() + "':'" + dependency.getArtifactId()
                                        + "' does not have version specified");
                    }

                    // String groupId, String artifactId, String version, String scope, String type
                    Artifact artifact = artifactFactory.createArtifact(dependency.getGroupId(), //
                            dependency.getArtifactId(), //
                            version, //
                            dependency.getScope(), //
                            dependency.getType());

                    artifactResolver.resolve(artifact, this.remoteRepos, this.local);
                    JarFile jarFile = new JarFile(artifact.getFile());
                    extractJarToArchive(jarFile, os, this.excludes);
                }
            }

            Manifest manifest = new Manifest();

            Manifest.Attribute mainClassAtt = new Manifest.Attribute();
            mainClassAtt.setName("Main-Class");
            mainClassAtt.setValue(mainClass);
            manifest.addConfiguredAttribute(mainClassAtt);

            manifest.write(tmpManifestWriter);
            tmpManifestWriter.flush();
            tmpManifestWriter.close();

            os.putArchiveEntry(new JarArchiveEntry("META-INF/MANIFEST.MF"));
            IOUtils.copy(new FileInputStream(tmpManifestFile), os);
            os.closeArchiveEntry();

            if (attachArtifact) {
                //MavenProject project, String artifactType, String artifactClassifier, File artifactFile
                projectHelper.attachArtifact(project, attachArtifactClassifierType, attachArtifactClassifier,
                        execWarJar);
            }

            if (extraResources != null) {
                for (ExtraResource extraResource : extraResources) {

                    DirectoryScanner directoryScanner = new DirectoryScanner();
                    directoryScanner.setBasedir(extraResource.getDirectory());
                    directoryScanner.addDefaultExcludes();
                    directoryScanner.setExcludes(toStringArray(extraResource.getExcludes()));
                    directoryScanner.setIncludes(toStringArray(extraResource.getIncludes()));
                    directoryScanner.scan();
                    for (String includeFile : directoryScanner.getIncludedFiles()) {
                        getLog().debug("include file:" + includeFile);
                        os.putArchiveEntry(new JarArchiveEntry(includeFile));
                        IOUtils.copy(new FileInputStream(new File(extraResource.getDirectory(), includeFile)),
                                os);
                        os.closeArchiveEntry();
                    }
                }
            }

            if (tomcatConfigurationFilesDirectory != null && tomcatConfigurationFilesDirectory.exists()) {
                // Because its the tomcat default dir for configs
                String aConfigOutputDir = "conf/";
                copyDirectoryContentIntoArchive(tomcatConfigurationFilesDirectory, aConfigOutputDir, os);
            }

        } catch (ManifestException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (ArchiveException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (ArtifactNotFoundException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (ArtifactResolutionException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(os);
            IOUtils.closeQuietly(tmpManifestWriter);
            IOUtils.closeQuietly(execWarJarOutputStream);
            IOUtils.closeQuietly(tmpPropertiesFileOutputStream);
        }
    }

    protected String findArtifactVersion(Dependency dependency) {
        // search in project.dependencies
        for (Dependency projectDependency : (List<Dependency>) this.project.getDependencies()) {
            if (sameDependencyWithoutVersion(dependency, projectDependency)) {
                return projectDependency.getVersion();
            }
        }

        // search in project.dependencies
        for (Dependency projectDependency : this.project.getDependencyManagement().getDependencies()) {
            if (sameDependencyWithoutVersion(dependency, projectDependency)) {
                return projectDependency.getVersion();
            }
        }

        return null;
    }

    protected boolean sameDependencyWithoutVersion(Dependency that, Dependency dependency) {
        return StringUtils.equals(that.getGroupId(), dependency.getGroupId()) && StringUtils.equals(
                that.getArtifactId(), dependency.getArtifactId());
    }

    protected void copyDirectoryContentIntoArchive(File sourceFolder, String destinationPath,
                                                   ArchiveOutputStream archiveOutputStream)
            throws IOException {

        // Scan the directory
        DirectoryScanner directoryScanner = new DirectoryScanner();
        directoryScanner.setBasedir(sourceFolder);
        directoryScanner.addDefaultExcludes();
        directoryScanner.scan();

        // Each File
        for (String includeFileName : directoryScanner.getIncludedFiles()) {
            getLog().debug("include configuration file : " + destinationPath + includeFileName);
            File inputFile = new File(sourceFolder, includeFileName);

            FileInputStream sourceFileInputStream = null;
            try {
                sourceFileInputStream = new FileInputStream(inputFile);

                archiveOutputStream.putArchiveEntry(new JarArchiveEntry(destinationPath + includeFileName));
                IOUtils.copy(sourceFileInputStream, archiveOutputStream);
                archiveOutputStream.closeArchiveEntry();
            } finally {
                IOUtils.closeQuietly(sourceFileInputStream);
            }
        }

    }

    /**
     * Resolves the plugin work dir as a sub directory of {@link #buildDirectory}, creating it if it does not exist.
     *
     * @return File representing the resolved plugin work dir
     * @throws MojoExecutionException if the plugin work dir cannot be created
     */
    protected File resolvePluginWorkDir()
            throws MojoExecutionException {
        if (!pluginWorkDirectory.exists() && !pluginWorkDirectory.mkdirs()) {
            throw new MojoExecutionException(
                    "Could not create plugin work directory at " + pluginWorkDirectory.getAbsolutePath());
        }

        return pluginWorkDirectory;

    }

    protected String[] toStringArray(List list) {
        if (list == null || list.isEmpty()) {
            return new String[0];
        }
        List<String> res = new ArrayList<String>(list.size());

        for (Iterator ite = list.iterator(); ite.hasNext(); ) {
            res.add((String) ite.next());
        }
        return res.toArray(new String[res.size()]);
    }


    /**
     * return file can be deleted
     */
    protected File addContextXmlToWar(File contextXmlFile, File warFile)
            throws IOException, ArchiveException {
        ArchiveOutputStream os = null;
        OutputStream warOutputStream = null;
        File tmpWar = File.createTempFile("tomcat", "war-exec");
        tmpWar.deleteOnExit();

        try {
            warOutputStream = new FileOutputStream(tmpWar);
            os = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.JAR, warOutputStream);
            os.putArchiveEntry(new JarArchiveEntry("META-INF/context.xml"));
            IOUtils.copy(new FileInputStream(contextXmlFile), os);
            os.closeArchiveEntry();

            JarFile jarFile = new JarFile(warFile);
            extractJarToArchive(jarFile, os, null);
            os.flush();
        } finally {
            IOUtils.closeQuietly(os);
            IOUtils.closeQuietly(warOutputStream);
        }
        return tmpWar;
    }

    /**
     * Copy the contents of a jar file to another archive
     *
     * @param file The input jar file
     * @param os   The output archive
     * @throws IOException
     */
    protected void extractJarToArchive(JarFile file, ArchiveOutputStream os, String[] excludes)
            throws IOException {
        Enumeration<? extends JarEntry> entries = file.entries();
        while (entries.hasMoreElements()) {
            JarEntry j = entries.nextElement();

            if (excludes != null && excludes.length > 0) {
                for (String exclude : excludes) {
                    if (SelectorUtils.match(exclude, j.getName())) {
                        continue;
                    }
                }
            }

            if (StringUtils.equalsIgnoreCase(j.getName(), "META-INF/MANIFEST.MF")) {
                continue;
            }
            os.putArchiveEntry(new JarArchiveEntry(j.getName()));
            IOUtils.copy(file.getInputStream(j), os);
            os.closeArchiveEntry();
        }
        if (file != null) {
            file.close();
        }
    }
}
